<?php

namespace Plugins\ApiDoc\Http\Admin\Service;

use App\Middlewares\Api\ApiAuthenticateMiddleware;
use App\Util\Helper;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\GetMapping;
use Hyperf\HttpServer\Annotation\Mapping;
use Hyperf\HttpServer\Annotation\PostMapping;
use Hyperf\HttpServer\PriorityMiddleware;
use Hyperf\HttpServer\Router\Handler;
use Plugins\ApiDoc\Attributes\Api;
use Plugins\ApiDoc\Attributes\ApiBody;
use Plugins\ApiDoc\Attributes\ApiQuery;
use Plugins\ApiDoc\Attributes\ApiReturn;
use Plugins\ApiDoc\Attributes\ApiVersion;
use Sc\Util\ImitateAopProxy\AopProxyTrait;
use Sc\Util\ScTool;
use Sc\Util\Tool;

/**
 * Class ApiDocService
 */
class ApiDocService
{
    use AopProxyTrait;

    private array $config = [];
    private array $allRouteMap = [];

    /**
     * @throws \Exception
     */
    public function get(): array
    {
        $appName = Helper::getConfig('app.app_name', '');
        $getPath = $this->getDefaultConfig('get_path');

        $this->allRouteMap = [
            'GET' => Helper::route()->getRouteList('http', 'GET'),
            'POST' => Helper::route()->getRouteList('http', 'POST'),
        ];

        $apiLists = $this->scan($getPath);

        $apiLists = $this->apiGroup($apiLists);

        return [
            "title"    => $appName . 'API 接口文档',
            "version"  => array_values(array_unique(array_merge(...array_column($apiLists, 'version')))),
            'host'     => ScTool::url(Helper::request()->url())->getDomain(),
            'apiLists' => $apiLists,
        ];
    }

    /**
     * @param string|null $key
     *
     * @return array|mixed
     */
    private function getDefaultConfig(string $key = null): mixed
    {
        if (!$this->config) {
            $this->config = Helper::getConfig('plugins.ApiDoc') ?: include __DIR__ . '/../../../config.php';
        }

        return $key === null ? $this->config : $this->config[$key];
    }

    /**
     * @param array $paths
     *
     * @return array
     * @throws \Exception
     */
    private function scan(array $paths): array
    {
        $apiList = [];

        foreach ($paths as ['path' => $path, 'namespace' => $namespace, ]) {
            try {
                ScTool::dir($path)->each(function (Tool\Dir\EachFile $eachFile) use ($path, $namespace, &$apiList){
                    $classname = strtr(basename($eachFile->filename), ['.php' => '']);
                    if (!str_ends_with($eachFile->filename, '.php') || !preg_match('/^[a-zA-Z1-9]+$/', $classname)){
                        return;
                    }

                    $classFullName = implode("\\", [$namespace, ...$eachFile->relativelyDirs, $classname]);

                    try {
                        if (!class_exists($classFullName)) {
                            return;
                        }
                        $reflectionClass = $this->apiClassCheck($classFullName);
                        if ($reflectionClass) {
                            $controller = $reflectionClass->getAttributes(Controller::class)[0]->newInstance();
                            $apiList[] = $this->apiResolve($reflectionClass, $controller);
                        }
                    } catch (\Throwable $throwable) {
                        var_dump($throwable->getMessage() . $throwable->getFile() . $throwable->getLine());
                    }
                });
            } catch (\Exception $exception) {}
        }

        return $apiList;
    }

    /**
     * @param $class
     *
     * @return \ReflectionClass|null
     * @throws \ReflectionException
     */
    private function apiClassCheck($class): ?\ReflectionClass
    {
        $reflexClass = new \ReflectionClass($class);

        if (!$reflexClass->getAttributes(Api::class) && !$reflexClass->getAttributes(Controller::class)) {
            return null;
        }

        return $reflexClass;
    }

    /**
     * @param \ReflectionClass $reflectionClass
     *
     * @return array
     */
    private function apiResolve(\ReflectionClass $reflectionClass, Controller $controller): array
    {
        $groupTitle = $this->apiTitleResolve($reflectionClass->getDocComment());
        $apis       = [];

        $baseUri = $controller->prefix;

        foreach ($reflectionClass->getMethods() as $reflectionMethod) {
            if ($api = $this->methodResolve($reflectionMethod, $baseUri)) {
                $apis[] = $api;
            }
        }


        return [
            'name'     => $groupTitle,
            'version'  => array_values(array_unique(array_merge(...array_column($apis, 'version')))),
            'children' => $apis,
        ];
    }

    /**
     * @param string $comment
     *
     * @return string
     */
    private function apiTitleResolve(string $comment): string
    {
        preg_match('/^\/(\*|\s)+([^*\s]+)/', $comment, $document);

        return empty($document[2]) ? "未命名" : $document[2];
    }

    /**
     * @param Handler $route
     *
     * @return string|null
     */
    private function authenticateType(Handler $route): ?string
    {
        /** @var PriorityMiddleware[] $haveMiddlewares */
        $haveMiddlewares = $route->options['middleware'] ?? [];

        foreach ($haveMiddlewares as $middleware) {
            if ($middleware->middleware === ApiAuthenticateMiddleware::class) {
                return "strength";
            }
             if ($middleware->middleware === ApiAuthenticateMiddleware::class) {
                return "strength";
            }
        }

        return null;
    }

    /**
     * @param \ReflectionMethod $reflectionMethod
     *
     * @return array|null
     */
    private function methodResolve(\ReflectionMethod $reflectionMethod, string $baseUri): ?array
    {
        if (!$reflectionMethod->getAttributes(Api::class)) {
            return null;
        }
        if (!($mapping = $reflectionMethod->getAttributes(GetMapping::class)) && !($mapping = $reflectionMethod->getAttributes(PostMapping::class))) {
            return null;
        }

        /** @var Mapping $mapping */
        $mapping = $mapping[0]->newInstance();
        $method  = current($mapping->methods);
        $call    = $this->getCallLocation($reflectionMethod);
        $url     = '/' . $baseUri . '/' . $mapping->path;
        $route   = $this->allRouteMap[$method][$url];

        $api = [
            'name'           => $this->apiTitleResolve($reflectionMethod->getDocComment()),
            'call'           => $call,
            'url'            => $url,
            'method'         => $method,
            'version'        => [1],
            'auth'           => $this->authenticateType($route),
            'requestParams'  => [],
            'responseParams' => [],
        ];

        foreach ($reflectionMethod->getAttributes() as $attribute) {
            $attribute = $attribute->newInstance();
            switch (true) {
                case $attribute instanceof ApiQuery:
                case $attribute instanceof ApiBody:
                    $api['requestParams'][] = json_decode(json_encode($attribute), true);
                break;
                case $attribute instanceof ApiReturn:
                    $api['responseParams'][] = json_decode(json_encode($attribute), true);
                    break;
                case $attribute instanceof ApiVersion && $attribute->version != 1:
                    $api['version'][] = $attribute->version;
            }
        }
        $api['responseParams'] = $this->childrenParamHandle($api['responseParams']);
        if ($api['method'] === 'POST') {
            $api['requestParams'] = $this->childrenParamHandle($api['requestParams']);
        }

        return $api;
    }

    /**
     * @param array $initialData
     *
     * @return array
     */
    private function childrenParamHandle(array $initialData): array
    {
        $newData = [];
        foreach ($initialData as $item) {
            $item['children'] = [];
            $names            = explode('.', $item['name']);
            $current          = &$newData;

            foreach (array_slice($names, 0, -1) as $attr) {
                $current = &$current[$attr]['children'];
            }

            $item['name']           = end($names);
            $current[$item['name']] = $item;
        }

        return $this->childrenData(array_values($newData));
    }

    /**
     * @param array $data
     *
     * @return array
     */
    private function childrenData(array $data): array
    {
        return array_map(function ($value){
            if ($value['children']){
                $value['children'] = array_values($this->childrenData($value['children']));
            }
            return $value;
        }, $data);
    }

    /**
     * @param \ReflectionMethod $method
     *
     * @return string
     */
    private function getCallLocation(\ReflectionMethod $method): string
    {
        return $method->getDeclaringClass()->getNamespaceName() . "\\"
            . $method->getDeclaringClass()->getShortName() . "@"
            . $method->getName();
    }

    /**
     * @param mixed $path
     *
     * @return array|string[]
     */
    private function getPaths(mixed $path): array
    {
        if (str_contains($path, '*')) {
            $MPaths = [];
            $paths = explode('*', $path);
            $last = array_pop($paths);
            foreach ($paths as $mp) {
                if (!$MPaths) {
                    $dirs   = ScTool::dir($mp)->getDirs();
                    $MPaths = array_map(fn($dir) => $mp . DIRECTORY_SEPARATOR . $dir, $dirs);
                    continue;
                }
                $MPaths = array_merge(...array_map(function ($tp) use ($mp) {
                    if (!is_dir($tp . DIRECTORY_SEPARATOR . $mp)) {
                        return [];
                    }

                    return ScTool::dir($tp . DIRECTORY_SEPARATOR . $mp)->getDirs();
                }, $MPaths));
            }

            $MPaths = array_filter(array_map(fn($tp) => is_dir($tp . DIRECTORY_SEPARATOR . $last) ? $tp . DIRECTORY_SEPARATOR . $last : '', $MPaths));
        } else {
            $MPaths = [$path];
        }
        return array_map(fn($path) => preg_replace('/(\/+|\\\+)/', DIRECTORY_SEPARATOR, $path),$MPaths);
    }

    private function loadRoute(): void
    {
        foreach ($this->getDefaultConfig('load_route') as $path){
            ScTool::dir($path)->each(function (Tool\Dir\EachFile $file){
                include $file->filepath;
            });
        }
    }

    /**
     * @param array $apiLists
     *
     * @return array
     */
    private function apiGroup(array $apiLists): array
    {
        $newList = [];

        foreach ($apiLists as $apiList) {
            $newList[$apiList['name']][] = $apiList;
        }

        return array_map(function ($list) {
            return [
                'name' => $list[0]['name'],
                'children' => array_merge(...array_column($list, 'children')),
                'version' => array_merge(...array_column($list,'version'))
            ];
        }, $newList);
    }
}